// FreeZzaphCLI.cs
// 
// Copyright © 2009 FreeZzaph
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program.  If not, see <http://www.gnu.org/licenses/>.
//

using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Text;
using LibFreeZzaph;

namespace FreeZzaphCLI
{
	sealed class FreeZzaphCLI
	{
		public static FreeZzaphConfiguration Settings;
		
		private FreeZzaph fz;
		
		private IDictionary<string, IFreeZzaphContentProviderPlugin> contentProviderPlugins;
		private IDictionary<string, IFreeZzaphPostProcessorPlugin> postProcessorPlugins;
		private IFreeZzaphContentProviderPlugin currentPlugin;
		
		private static string GenerateDirectoryPath(string directoryFormat, IDictionary<string, string> directoryReplacementValues)
		{
			string directory = directoryFormat;

			foreach (string key in directoryReplacementValues.Keys)
			{
				directory = directory.Replace("%" + key + "%", directoryReplacementValues[key]);
			}
			
			return directory;
		}
		
		private static string[] ParseCommandLineInput(string input)
		{
			char[] inputc = input.ToCharArray();
			List<string> parameters = new List<string>();
			StringBuilder parameter = new StringBuilder();
			bool isInString = false;
			
			for (int i = 0; i < inputc.Length; i++)
			{
				if (!isInString && char.IsWhiteSpace(inputc[i]))
				{
					if (parameter.Length > 0)
					{
						parameters.Add(parameter.ToString());
						parameter.Remove(0, parameter.Length);
					}
				}
				else if (inputc[i] == '"')
				{
					if (!isInString)
					{
						if (parameter.Length == 0)
							isInString = true;
						else
							return null;
					}
					else
					{
						parameters.Add(parameter.ToString());
						parameter.Remove(0, parameter.Length);
						isInString = false;
					}              
				}
				else
				{
					parameter.Append(inputc[i]);
				}
			}
			
			if (!isInString)
			{
				if (parameter.Length != 0)
				{
					parameters.Add(parameter.ToString());
				}
				return parameters.ToArray();
			}
			
			return null;
		}
		
		private static string AppPath
		{
			get
			{
				string assemblyLocation = System.Reflection.Assembly.GetEntryAssembly().Location;
				string assemblyFolder = System.IO.Path.GetDirectoryName(assemblyLocation);
				
				return assemblyFolder;
			}
		}
		
		public void AppLoop() {
			string input = "";
			while (true) {
				if (currentPlugin == null)
				{
					Console.Write("FreeZzaph> ");
				}
				else
				{
					Console.Write(string.Format("FreeZzaph/{0}> ", currentPlugin.Name));
				}
				
				try {
					input = Console.ReadLine();
				}
				catch (Exception e)
				{
					Console.WriteLine("An error occured while attempting to read from the keyboard:");
					Console.WriteLine(e.Message);
					continue;
				}
				
				string[] splitInput = ParseCommandLineInput(input);
				if (splitInput == null)
				{
					Console.WriteLine("Invalid input. Make sure you close your quotes.");
					Console.WriteLine();
					continue;
				}

				if (splitInput.Length == 0)
					continue;
				
				switch (splitInput[0])
				{
					case "quit":
					case "q":
					case "exit":
					case "x":
						if (splitInput.Length != 1)
						{
							Console.WriteLine("Usage: quit");
							Console.WriteLine();
							continue;
						}
						
						// Quitting: Leaving application loop
						return;
						
					case "list":
						ListPlugins(splitInput);
						Console.WriteLine();
						break;
						
					case "use":
					case "select":
						UsePlugin(splitInput);
						Console.WriteLine();
						break;
						
					case "category":
						PluginCategories(splitInput);
						Console.WriteLine();
						break;
						
					case "fetch":
					case "collect":
					case "download":
						FetchFiles(splitInput);
						Console.WriteLine();
						break;
						
					case "settings":
						ConfigureSettings(splitInput);
						Console.WriteLine();
						break;
					
					case "help":
						Console.WriteLine(
@"quit                        Quits the application
list                        Lists the available content provider plug-ins
use <plug-in>               Sets the given plug-in as the current content
                            provider plug-in
category                    Lists the current plug-in's category groups
category <group>            Lists the categories contained in the given category
                            group
fetch <group> <category>    Downloads files from the given category group and
                            category.
fetch <g> <c> <count>       Downloads files from the given category group and
                            category, requesting the given amount of pages from
                            the plug-in.
fetch <g> <ca> <co> <start> Downloads files from the given category group and
                            category, requesting the given amount of pages from
                            the plug-in, starting from the given position.
settings list               Lists the settings that are available to the
                            application
settings get <setting>      Shows the current value of the given setting
settings set <set> <value>  Sets the value of the given setting to the given
                            value
settings reset <setting>    Resets the value of the given setting to the
                            application default.");
						Console.WriteLine();
						break;
					
					default:
						List<string> parameterList = new List<string>();
						for (int i = 1; i < splitInput.Length; i++)
						{
							parameterList.Add(splitInput[i]);
						}
						
						if (currentPlugin == null || !fz.IssuePluginCommand(currentPlugin.GetType(), splitInput[0], parameterList))
						{
							Console.WriteLine(string.Format("Unknown command '{0}'", splitInput[0]));
							Console.WriteLine();
						}
						break;
				}
			}
		}
		
		private void ListPlugins(string[] inputList)
		{
			if (inputList.Length != 1)
			{
				Console.WriteLine("Usage: list");
				return;
			}
			
			// Listing the content provider plug-ins
			if (contentProviderPlugins.Count > 0)
			{
				Console.WriteLine("List of available content provider plug-ins:");
				foreach (IFreeZzaphContentProviderPlugin p in contentProviderPlugins.Values)
				{
					Console.WriteLine(string.Format("{0} - {1}", p.Name, p.Title));
				}
			}
			else
			{
			Console.WriteLine("No content provider plug-ins are loaded.");	
			}
			
			if (postProcessorPlugins.Count > 0)
			{
				Console.WriteLine();
				Console.WriteLine("List of active post-processor plug-ins:");
				foreach (IFreeZzaphPostProcessorPlugin p in postProcessorPlugins.Values)
				{
					Console.WriteLine(string.Format("{0}", p.Name));
				}
			}
			
		}
		
		private void UsePlugin(string[] inputList)
		{
			if (inputList.Length != 2)
			{
				Console.WriteLine("Usage: use <plug-in name>");
				return;
			}
			
			// Switch to a different plug-in
			IFreeZzaphContentProviderPlugin newPlugin;
			if (!contentProviderPlugins.TryGetValue(inputList[1], out newPlugin))
			{
				Console.WriteLine(string.Format("No plug-in '{0}' is loaded. Use the 'list' command to see the loaded plugins.", inputList[1]));
			}
			else
			{
				Console.WriteLine(string.Format("Switching to the '{0}' plug-in.", newPlugin.Name));
				currentPlugin = newPlugin;
			}
		}
		
		private void PluginCategories(string[] inputList)
		{
			if (currentPlugin == null)
			{
				Console.WriteLine("No plug-in has been selected. Use the 'use' command to select a plugin for use.");
				return;
			}
			
			if (inputList.Length == 1)
			{
				IDictionary<string, CategoryGroup> categoryGroups = currentPlugin.CategoryGroups;
				if (categoryGroups == null || categoryGroups.Count == 0)
				{
					Console.WriteLine("The plug-in does not provide any category groups.");
					return;
				}
				
				// Listing the category groups
				Console.WriteLine(string.Format("List of category groups in {0}:", currentPlugin.Name));
				foreach (CategoryGroup cg in categoryGroups.Values)
				{
					Console.WriteLine(string.Format("{0} - {1}", cg.Type, cg.Description));
				}
			}
			else if (inputList.Length == 2)
			{
				IDictionary<string, CategoryGroup> categoryGroups = currentPlugin.CategoryGroups;
				if (categoryGroups == null || categoryGroups.Count == 0)
				{
					Console.WriteLine("The plug-in does not provide any category groups.");
					return;
				}
				
				CategoryGroup categoryGroup;
				if (categoryGroups.TryGetValue(inputList[1], out categoryGroup))
				{
					IDictionary<string, Category> categories = categoryGroup.Categories;
					
					if (categories == null || categories.Count == 0)
					{
						Console.WriteLine("The category group does not contain any categories.");
						return;
					}
					
					// Listing the categories
					Console.WriteLine(string.Format("List of categories in {0}:", categoryGroup.Type));
					List<string> categoryTypes = new List<string>(categories.Count);
					foreach (Category c in categories.Values)
					{
						categoryTypes.Add(string.Format("{0} - {1}", c.Name, c.Description));
					}
					categoryTypes.Sort();
					foreach (string s in categoryTypes)
					{
						Console.WriteLine(s);
					}
				}
				else
				{
					Console.WriteLine(string.Format("The plug-in does not provide any category group named '{0}'.", inputList[1]));
					return;	
				}
			}
			else
			{
				Console.WriteLine("Usage: category [category group]");
				return;
			}
		}
		
		private void FetchFiles(string[] inputList)
		{
			if (currentPlugin == null)
			{
				Console.WriteLine("No plug-in has been selected. Use the 'use' command to select a plugin for use.");
				return;
			}
			
			int count = -1;
			uint start = 0;
			if (inputList.Length < 3 || inputList.Length > 5)
			{
				Console.WriteLine("Usage: fetch <category group> <category> [<count> [<start>]]");
				return;
			}
			else if (inputList.Length == 4)
			{
				if (!int.TryParse(inputList[3], out count))
				{
					Console.WriteLine("Usage: fetch <category group> <category> [<count> [<start>]]");
					return;
				}
			}
			else if (inputList.Length == 5)
			{
				if (!int.TryParse(inputList[3], out count) || !uint.TryParse(inputList[4], out start))
				{
					Console.WriteLine("Usage: fetch <category group> <category> [<count> [<start>]]");
					return;
				}
			}
			
			// Get the list of categories
			IDictionary<string, CategoryGroup> categoryGroups = currentPlugin.CategoryGroups;

			if (categoryGroups == null || categoryGroups.Count == 0)
			{
				Console.WriteLine("The plug-in does not provide any category groups.");
				return;
			}
			
			CategoryGroup categoryGroup;
			if (!categoryGroups.TryGetValue(inputList[1], out categoryGroup))
			{
				Console.WriteLine(string.Format("The plug-in does not provide any category group named '{0}'.", inputList[1]));
				return;
			}
			
			IDictionary<string, Category> categories = categoryGroup.Categories;						
			if (categories == null || categories.Count == 0)
			{
				Console.WriteLine("The category group does not contain any categories.");
				return;
			}
			
			Category category;
			if (!categories.TryGetValue(inputList[2], out category))
			{
				Console.WriteLine(string.Format("The category group does not contain any category named '{0}'.", inputList[2]));
				return;
			}
			
			Console.Write(string.Format("Requesting URLs from '{0}'...", currentPlugin.Name));
			
			IList<Uri> urls;
			try
			{
				urls = category.FetchUris(start, count);
			}
			catch (PluginException pe)
			{
				Console.WriteLine("failed.");
				Console.WriteLine("A problem occured while gathering URLs:");
				Console.WriteLine(pe.Message);
				return;
			}
			catch (Exception e)
			{
				Console.WriteLine("failed.");
				Console.WriteLine("An unexpected exception was caught:");
				Console.WriteLine(e.Message);
				return;
			}
			
			if (urls != null)
			{
				Console.WriteLine(string.Format("{0} received.", urls.Count));
			}
			else
			{
				Console.WriteLine("failed!");
				return;
			}
			
			string downloadFolder = Settings["DownloadBaseDirectory"];
			downloadFolder = Path.Combine(AppPath, downloadFolder);
			if (!Directory.Exists(downloadFolder))						
				Directory.CreateDirectory(downloadFolder);
			
			IDictionary<string,string> directoryReplacementValues = new Dictionary<string,string>();
			
			directoryReplacementValues.Add("plugin", currentPlugin.Name);
			directoryReplacementValues.Add("categorygroup", categoryGroup.Type);
			directoryReplacementValues.Add("category", category.Name);
			directoryReplacementValues.Add("datetime", DateTime.Now.ToString("yyyy-MM-dd_HHmmss"));
			
			string directoryFormat = Settings["SaveDirectory"];
			string saveDirectory = GenerateDirectoryPath(directoryFormat, directoryReplacementValues);
			string currentDownloadFolder = Path.Combine(downloadFolder, saveDirectory);
			
			if (!Directory.Exists(currentDownloadFolder))						
				Directory.CreateDirectory(currentDownloadFolder);
			
			Downloader downloader = new Downloader();
			downloader.SavePath = currentDownloadFolder;
			downloader.DownloadFrom(urls, category.Filter);
			IList<string> files = downloader.Files;
			
			Console.WriteLine(string.Format("Downloaded {0} files.", files.Count));
			Console.WriteLine();
			foreach (IFreeZzaphPostProcessorPlugin postProcessorPlugin in postProcessorPlugins.Values)
			{
				Console.Write(string.Format("{0}...", postProcessorPlugin.Action));
				try
				{
					files = postProcessorPlugin.PerformPostProcessing(files);
				}
				catch (Exception e)
				{
					Console.WriteLine("failed:");
					Console.WriteLine(e.Message);
					Console.WriteLine();
					continue;
				}
				Console.WriteLine("done.");
			}
		}
		
		public void ConfigureSettings(string[] inputList)
		{
			if (inputList.Length < 2)
			{
				Console.WriteLine("Usage: settings list");
				Console.WriteLine("       settings get <setting>");
				Console.WriteLine("       settings set <setting> <value>");
				Console.WriteLine("       settings reset <setting>");
				return;
			}
			
			switch (inputList[1])
			{
				case "list":
					if (inputList.Length != 2)
					{
						Console.WriteLine("Usage: settings list");
						return;
					}
					Console.WriteLine("Available settings:");
					IList<string> keys = Settings.Keys;
					foreach (string key in keys)
						Console.WriteLine(key);
					break;
					
				case "get":
					if (inputList.Length != 3)
					{
						Console.WriteLine("Usage: settings get <setting>");
						return;
					}
					if (Settings[inputList[2]] != null) {
						Console.WriteLine(string.Format("Value of setting {0}:", inputList[2]));
						Console.WriteLine(Settings[inputList[2]]);
					}
					else
					{
						Console.WriteLine("There is no setting named {0}.", inputList[2]);
					}
					break;
					
				case "set":
					if (inputList.Length != 4)
					{
						Console.WriteLine("Usage: settings set <setting> <value>");
						return;
					}
					if (Settings[inputList[2]] != null) {
						Settings[inputList[2]] = inputList[3];
						Console.WriteLine(string.Format("Setting {0} was set to:", inputList[2]));
						Console.WriteLine(Settings[inputList[2]]);
					}
					else
					{
						Console.WriteLine("There is no setting named {0}.", inputList[2]);
					}
					break;
					
				case "reset":
					if (inputList.Length != 3)
					{
						Console.WriteLine("Usage: settings reset <setting>");
						return;
					}
					if (Settings[inputList[2]] != null) {
						Settings.Reset(inputList[2]);
						Console.WriteLine(string.Format("Value of setting {0} was reset to:", inputList[2]));
						Console.WriteLine(Settings[inputList[2]]);
					}
					else
					{
						Console.WriteLine("There is no setting named {0}.", inputList[2]);
					}
					break;
					
			}
		}
		
		public void Run()
		{
			string version = System.Reflection.Assembly.GetExecutingAssembly().GetName().Version.ToString();
			Console.WriteLine(string.Format(
@"FreeZzaph v{0} Copyright © 2009 FreeZzaph
This program comes with ABSOLUTELY NO WARRANTY; for details see
the COPYING file.
", version));
			
			Console.Write("Loading plug-ins...");

			// Load plug-ins
			string pluginDir = Settings["PluginDirectory"];
			pluginDir = Path.Combine(AppPath, pluginDir);
			
			fz = new FreeZzaph();
			try
			{
				fz.LoadPlugins(pluginDir);
				Console.WriteLine("done!");
			}
			catch (LoadPluginException lpe)
			{
				if (lpe.ExceptionList == null)
				{
					// Plugin directory not found
					Console.WriteLine("failed!");
					
					Console.WriteLine(string.Format("There was an error loading the plug-ins:\n{0}", lpe.Message));
				}
				else
				{
					// List of exceptions provided
					Console.WriteLine();
					Console.WriteLine("Some errors occured while loading the plug-ins:");
					foreach (LoadPluginException e in lpe.ExceptionList)
					{
						Console.WriteLine(string.Format(" -\t{0}", e.Message));
					}
				}
			}
			finally
			{
				contentProviderPlugins = fz.ContentProviderPlugins;
				postProcessorPlugins = fz.PostProcessorPlugins;
				Console.WriteLine(string.Format("Loaded {0} content provider plug-ins", contentProviderPlugins.Count));
				Console.WriteLine(string.Format("Loaded {0} post-processor plug-ins", fz.PostProcessorPlugins.Count));
				Console.WriteLine();
			}
			
			AppLoop();
		}
		
		public static void Main(string[] args)
		{
			FreeZzaphCLI.Settings = FreeZzaphConfiguration.Instance;
			FreeZzaphCLI cli = new FreeZzaphCLI();
			cli.Run();
		}
	}
}